﻿// reference: https://github.com/omacarena/novaburst.ModularTypeScript/blob/master/novaburst.ModularTypeScript/NovaBurst.ModularTypeScript.Core.UI/modules/Core/Module/moduleLoader.ts
///<amd-dependency path="jquery" />
import Core = Sustainalytics.Module;

export function get_browser_info(): any {
    var ua = navigator.userAgent, tem, M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
    if (/trident/i.test(M[1])) {
        tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
        return 'IE ' + (tem[1] || '');
    }
    if (M[1] === 'Chrome') {
        tem = ua.match(/\bOPR\/(\d+)/)
        if (tem != null) { return 'Opera ' + tem[1]; }
    }
    M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
    if ((tem = ua.match(/version\/(\d+)/i)) != null) { M.splice(1, 1, tem[1]); }
    return {
        browser: M[0],
        version: M[1]
    };
}


// RequireJS plugin for loading modules and bundles
export function load(name, parentRequire, onload, config): void {

    // create loader
    var loader = new Loader(Core.ModuleLoaderConfig.current);

    // load module
    var promise = loader.loadModule(name);

    // success
    promise.done(function () {
        loader.logStatus();
        onload(null);
    });

    // error
    promise.fail(function () {
        loader.logStatus();
        onload.error.apply(onload, arguments);
    });
}


export class Loader {

    // modules that have been processed
    public static processedModules: any = {};

    // operation log
    private loadModuleName: string;
    private loadModuleSec: number = 0;
    private scriptsLoaded: string[] = [];
    private scriptsErrors: string[] = [];
    private scriptsSkipped: string[] = [];


    constructor(
        private config: Core.ModuleLoaderConfig) {
    }


    public logStatus() {
        var ctx = this;

        var statusObj =
            {
                time: ctx.loadModuleSec,
                loaded: ctx.scriptsLoaded,
                errors: ctx.scriptsErrors,
                skipped: ctx.scriptsSkipped
            };

        var scriptsLoadedCount = ctx.scriptsLoaded ? ctx.scriptsLoaded.length : 0;
        var scriptsErrorsCount = ctx.scriptsErrors ? ctx.scriptsErrors.length : 0;
        var scriptsSkippedCount = ctx.scriptsSkipped ? ctx.scriptsSkipped.length : 0;

        console.info(
            (scriptsErrorsCount > 0 ? 'ERR ' : 'OK! ') + '['
            + ('     ' + statusObj.time.toString()).slice(-6) + 's] ['
            + ('     ' + scriptsLoadedCount).slice(-3) + ' js] ['
            + ('     ' + scriptsSkippedCount).slice(-3) + ' skip] ['
            + ('     ' + scriptsErrorsCount).slice(-3) + ' err]   '
            + ctx.loadModuleName, statusObj);
    }

    /*
     * Load a specific module.
     */
    public loadModule(modName: string): JQueryPromise<any> {
        var ctx = this;

        ctx.loadModuleName = modName;

        var startDate = new Date();

        // get scripts count
        var moduleScriptsCountPromise = ctx.getModuleScriptsCount(modName);


        // create loader progress
        var loaderProgress = new LoaderProgress(ctx.config, moduleScriptsCountPromise);

        var cnt = 0;

        // process the module and load each script
        var processModulePromise = ctx.processModule(modName,(script) => {
            var loadScriptPromise = ctx.loadModuleScript(script, Loader.processedModules);

            // report script progress
            loaderProgress.reportScriptProgress(loadScriptPromise);

            loadScriptPromise.done(function () {
                cnt++;
                //console.log('LOAD: (' + cnt + ') - ' + script.path);
            });

            return loadScriptPromise;

        }, Loader.processedModules);


        // report module progress
        loaderProgress.reportModuleProgress(processModulePromise);


        processModulePromise.done(function () {

            var endDate = new Date();
            ctx.loadModuleSec = (endDate.getTime() - startDate.getTime()) / 1000.0;
        });

        return processModulePromise;
    }

    /*
     * Get total count of all module scripts.
     */
    public getModuleScriptsCount(modName: string): JQueryPromise<number> {
        var ctx = this;

        var def = $.Deferred();

        var count = 0;

        var processedMap = {};

        // process the module and count each script
        var processModulePromise = ctx.processModule(modName,(script) => {
            count++;
            //console.log('COUNT: (' + count + ') - ' + script.path);
            return $.when(null);
        }, processedMap);

        // success
        processModulePromise.done(function () {
            def.resolve(count);
        });

        // error
        processModulePromise.fail(function () {
            def.resolve();
        });

        return def;
    }

    private processModule(modName: string, processScriptFunc: (script: Core.IModuleDefinitionScript) => JQueryPromise<any>, processedMap: any): JQueryPromise<any> {
        var ctx = this;

        var def = $.Deferred();

        var loadPromise = Core.ModuleLoaderConfig.current.getLoadFromJsonPromise();

        loadPromise.done(function () {

            //var def: JQueryDeferred<any> = processedMap[modName.toLowerCase()];
            //if (def) {
            //    return def;
            //}
            //else {
            //    def = $.Deferred();
            //    processedMap[modName.toLowerCase()] = def;
            //}

            // try obtaining the bundle map
            var bundleMap = ctx.config.getModuleBundleMapInfo(modName);

            // if the bundle map was found then use its definition else search for a module with the given name
            if (bundleMap) {

                var loadBundlePromise = ctx.processModuleBundle(bundleMap, processScriptFunc, processedMap);

                // success
                loadBundlePromise.done(function () {
                    def.resolve();
                });

                // error
                loadBundlePromise.fail(function () {
                    def.resolve();
                });
            }
            else {

                // search for module
                var modDef = ctx.config.getModuleDefinitionInfo(modName);

                // if bundling is enabled then load all bundles containing current module else load the module files individually
                if (modDef) {

                    var processModDefPromise = ctx.processModuleDefinition(modDef, processScriptFunc, processedMap);

                    // success
                    processModDefPromise.done(function () {
                        def.resolve();
                    });

                    // error
                    processModDefPromise.fail(function () {
                        def.resolve();
                    });
                }
                else {

                    // process as a simple script otherwise
                    var processScriptPromise = ctx.processScripts([{ path: modName, browser: [] }], processScriptFunc, processedMap);

                    // success
                    processScriptPromise.done(function () {
                        def.resolve();
                    });

                    // error
                    processScriptPromise.fail(function () {
                        def.resolve();
                    });
                }
            }
        });

        return def;
    }

    private processModuleBundle(bundleMapInfo: Core.IModuleBundleMapInfo, processScriptFunc: (script: Core.IModuleDefinitionScript) => JQueryPromise<any>, processedMap: any): JQueryPromise<any> {
        var ctx = this;

        //var def = $.Deferred();

        var processedMapKey = 'bundle :: ' + bundleMapInfo.moduleBundleMap.relativeOutPath.toLowerCase();
        var def = processedMap[processedMapKey];
        if (def) {
            return def;
        }
        else {
            def = $.Deferred();
            processedMap[processedMapKey] = def;
        }

        // process bundle dependencies
        var bundleDepPromise = ctx.processModuleBundleDependencies(bundleMapInfo, processScriptFunc, processedMap);

        bundleDepPromise.done(function () {

            // if bundling is allowed then load the script
            if (ctx.config.useBundles) {

                var scriptToProcess: Core.IModuleDefinitionScript =
                    {
                        path: bundleMapInfo.moduleBundleMap.relativeOutPath,
                        browser: bundleMapInfo.moduleBundleMap.browser
                    };

                // load bundle script
                var processScriptPromise = ctx.processScripts([scriptToProcess], processScriptFunc, processedMap);

                // success
                processScriptPromise.done(function () {

                    def.resolve();
                });

                // error
                processScriptPromise.fail(function () {
                    def.resolve();
                });
            }
            else {

                var scriptInfoLst = ctx.createScripts(bundleMapInfo.moduleBundleMap.relativeFilePaths, bundleMapInfo.moduleBundleMap.browser);

                // load module bundle map files
                var processScriptsPromise = ctx.processScripts(scriptInfoLst, processScriptFunc, processedMap);

                // success
                processScriptsPromise.done(function () {
                    def.resolve();
                });

                // error
                processScriptsPromise.fail(function () {
                    def.resolve();
                });
            }
        });

        return def;
    }

    private createScripts(scripts: string[], browser: Core.IScriptBrowser[]): Core.IModuleDefinitionScript[] {
        var ctx = this;

        var scriptInfoLst: Core.IModuleDefinitionScript[] = [];

        if (scripts && scripts.length > 0) {

            scripts.forEach(script => {

                scriptInfoLst.push(
                    {
                        path: script,
                        browser: browser
                    });
            });
        }

        return scriptInfoLst;
    }

    private processModuleBundleDependencies(bundleMapInfo: Core.IModuleBundleMapInfo, processScriptFunc: (script: Core.IModuleDefinitionScript) => JQueryPromise<any>, processedMap: any) {
        var ctx = this;

        var def = $.Deferred();

        // bundle modules
        var modules = bundleMapInfo.moduleBundleMap.modules;

        if (!modules || modules.length == 0) {
            def.resolve();
            return def;
        }

        var processModulePromises: JQueryPromise<any>[] = [];


        // a list of dependencies
        var dependencies = [];


        // create a list of module dependencies if are not set to be ignored
        if (!bundleMapInfo.moduleBundleMap.ignoreModuleDependencies) {
            modules.forEach(modName => {

                // get module
                var mod = ctx.config.getModuleDefinitionInfo(modName);

                // add 
                if (mod && mod.moduleDefinition.dependencies && mod.moduleDefinition.dependencies.length > 0) {

                    // load each dependency
                    mod.moduleDefinition.dependencies.forEach(depName => {

                        dependencies.push(depName);
                    });
                }
            });
        }


        // add bundle custom dependencies
        if (bundleMapInfo.moduleBundleMap.dependencies && bundleMapInfo.moduleBundleMap.dependencies.length > 0) {
            dependencies.push.apply(dependencies, bundleMapInfo.moduleBundleMap.dependencies);
        }


        

        // process dependencies
        if (dependencies.length > 0) {

            var depsNames = '';

            dependencies.forEach(depName => {

                depsNames += ', ' + depName;

                // load dependency
                var processModPromise = ctx.processModule(depName, processScriptFunc, processedMap);

                processModulePromises.push(processModPromise);
            });
        }
        else {

            processModulePromises.push($.when(null));
        }


        $.when.apply($, processModulePromises)
            .done(function () {
            def.resolve();
        })
            .fail(function () {
            def.resolve();
        });


        return def;
    }

    private processModuleDefinition(modDefInfo: Core.IModuleDefinitionInfo, processScriptFunc: (script: Core.IModuleDefinitionScript) => JQueryPromise<any>, processedMap: any): JQueryPromise<any> {
        var ctx = this;

        //var def = $.Deferred();

        var processedMapKey = 'module :: ' + modDefInfo.moduleDefinition.moduleName.toLowerCase();
        var def = processedMap[processedMapKey];
        if (def) {
            return def;
        }
        else {
            def = $.Deferred();
            processedMap[processedMapKey] = def;
        }

        // whether to process all module associated bundle maps or only the module scripts
        if (ctx.config.useBundles) {

            var processBundleMapPromises: JQueryPromise<any>[] = [];

            // add all bundle maps scripts
            if (modDefInfo.moduleBundleMaps && modDefInfo.moduleBundleMaps.length > 0) {
                modDefInfo.moduleBundleMaps.forEach(modBundleMap => {

                    // get module bundle map info
                    var modBundleMapInfo = ctx.config.getModuleBundleMapInfo(modBundleMap.relativeOutPath);

                    if (!modBundleMapInfo) {
                        console.warn('ModuleLoader :: No bundle map info found for ' + modBundleMap.relativeOutPath);
                    }

                    // process bundle map
                    var promise = ctx.processModuleBundle(modBundleMapInfo, processScriptFunc, processedMap);

                    processBundleMapPromises.push(promise);
                });
            }
            else {
                processBundleMapPromises.push($.when(null));
            }

            // process all bundle maps
            var processBundleMapPromisesAll = $.when.apply($, processBundleMapPromises);

            // success
            processBundleMapPromisesAll.done(function () {
                def.resolve();
            });

            // error
            processBundleMapPromisesAll.fail(function () {
                def.resolve();
            });
        }
        else {

            var processDependenciesPromise = $.Deferred();

            // first process module dependencies
            if (modDefInfo.moduleDefinition.dependencies && modDefInfo.moduleDefinition.dependencies.length > 0) {

                var processModulePromiseLst = [];

                modDefInfo.moduleDefinition.dependencies.forEach(depModName => {
                    var processModulePromise = ctx.processModule(depModName, processScriptFunc, processedMap);
                    processModulePromiseLst.push(processModulePromise);
                });

                $.when.apply($, processModulePromiseLst)
                    .done(function () {
                    processDependenciesPromise.resolve();
                })
                    .fail(function () {
                    processDependenciesPromise.resolve();
                });
            }
            else {
                processDependenciesPromise.resolve();
            }


            processDependenciesPromise.always(function () {

                // process module scripts
                var processScriptsPromise = ctx.processScripts(modDefInfo.moduleDefinition.scriptsTyped, processScriptFunc, processedMap);

                // success
                processScriptsPromise.done(function () {
                    def.resolve();
                });

                // error
                processScriptsPromise.fail(function () {
                    def.resolve();
                });
            });
        }

        return def;
    }

    private processNextScript(scriptsToProcess: any, processedMap: any, processScriptFunc: any, def: any) {
        var ctx = this;

        if (scriptsToProcess.length > 0) {

            // remove and get first element from array
            var script = scriptsToProcess.shift();

            // determine if current script is already in process
            var processedMapKey = 'script :: ' + script.path.toLowerCase();
            var processScriptPromise: JQueryPromise<any> = processedMap[processedMapKey];

            if (!processScriptPromise) {

                processScriptPromise = processScriptFunc(script);
                processedMap[processedMapKey] = processScriptPromise;
            }

            // wait for script to load before processing the next
            processScriptPromise.always(function () {
                ctx.processNextScript(scriptsToProcess, processedMap, processScriptFunc, def);
            });
        }
        else {
            def.resolve();
        }
    }

    private processScripts(scripts: Core.IModuleDefinitionScript[], processScriptFunc: (script: Core.IModuleDefinitionScript) => JQueryPromise<any>, processedMap: any): JQueryPromise<any> {
        var ctx = this;

        var def = $.Deferred();

        if (scripts && scripts.length > 0) {

            var promises = [];

            // clone scripts
            var scriptsToProcess = scripts.slice(0);

            // process next script in sequence
        

            // process next script
            ctx.processNextScript(scriptsToProcess, processedMap, processScriptFunc, def);
        }
        else {
            def.resolve();
        }

        return def;
    }

    private loadModuleScript(script: Core.IModuleDefinitionScript, processedMap: any): JQueryPromise<any> {

        var ctx = this;

        var def = $.Deferred();

        // check if script should be fetched for current browser
        var isBrowserCompatible = ctx.checkScriptBrowserSpec(script.browser);


        if (isBrowserCompatible) {
            try
            {
                require([script.path], 
                    // success
                    function () {

                        ctx.scriptsLoaded.push(script.path);

                        def.resolve();
                    },
                    // error
                    function () {

                        ctx.scriptsErrors.push(script.path);

                        def.resolve();
                    });
            }
            catch (err) {
                ctx.scriptsErrors.push(script.path);
                ctx.logStatus();
                throw err;
            }
        }
        else {
            ctx.scriptsSkipped.push(script.path + ' >>> due to browser not meeting ' + JSON.stringify(script.browser));
            def.resolve();
        }

        return def;
    }

    private checkScriptBrowserSpec(browserList: Core.IScriptBrowser[]): boolean {

        // if browser spec exists then check browser
        if (browserList && browserList.length > 0) {

            // get browser info
            var browserInfo = get_browser_info();

            // determine if current browser has a match
            var isMatch = false;

            for (var index = 0; index < browserList.length; index++) {

                var browser = browserList[index];

                if (browserInfo.browser === browser.name) {
                    if (browser.vers && browserInfo.version) {

                        var currentVers = ('00000000000' + browserInfo.version.toString()).slice(-10);
                        var reqVers = ('00000000000' + browser.vers.toString()).slice(-10);

                        if (browser.comp) {
                            switch (browser.comp) {
                                case 'lte':
                                    isMatch = currentVers <= reqVers;
                                    break;
                                case 'lt':
                                    isMatch = currentVers < reqVers;
                                    break;
                                case 'gte':
                                    isMatch = currentVers >= reqVers;
                                    break;
                                case 'gt':
                                    isMatch = currentVers > reqVers;
                                    break;
                            }
                        }
                        else {
                            isMatch = currentVers == reqVers;
                        }
                    }
                    else {
                        isMatch = true;
                    }
                }

                if (browser.inverse) {
                    isMatch = !isMatch;
                }

                if (isMatch) {
                    break;
                }
            }

            // if file is not a match then mark the file as processed
            return isMatch;
        }
        else {
            return true;
        }
    }
}


export class LoaderProgress {

    // current progress
    public static progressDoneCount: number = 0;
    public static progressTotalCount: number = 0;

    // timeout handle
    public static reportOverallProgressTimeoutHandle: any = null;


    private addedToProgressDoneCount: number = 0;
    private addedToProgressTotalCount: number = 0;

    // scripts remaining for current module
    private remainingScriptsCount: number = 0;

    // whether the progress data was initialized
    private isInit: boolean = false;

    // whether the module was loaded
    private isLoaded: boolean = false;


    constructor(
        private config: Core.ModuleLoaderConfig,
        private scriptsCountPromise: JQueryPromise<number>) {

        var ctx = this;

        // initialize by obtaining the total number of scripts
        ctx.init(scriptsCountPromise);
    }


    private init(scriptsCountPromise: JQueryPromise<number>) {
        var ctx = this;

        scriptsCountPromise.done(cnt => {
            if (!ctx.isLoaded) {
                ctx.remainingScriptsCount += cnt;
                LoaderProgress.progressTotalCount += ctx.remainingScriptsCount;
                ctx.addedToProgressTotalCount = ctx.remainingScriptsCount;
                ctx.isInit = true;
            }
        });
    }

    public reportScriptProgress(scriptPromise: JQueryPromise<any>) {
        var ctx = this;

        // when the script was done loading
        scriptPromise.done(function () {
            ctx.remainingScriptsCount--;

            if (ctx.isInit) {
                ctx.addedToProgressDoneCount++;
                LoaderProgress.progressDoneCount++;
                ctx.reportOverallProgress();
            }
        });
    }

    public reportModuleProgress(modulePromise: JQueryPromise<any>) {
        var ctx = this;

        modulePromise.done(function () {
            if (ctx.isInit) {
                LoaderProgress.progressDoneCount -= ctx.addedToProgressDoneCount;
                LoaderProgress.progressTotalCount -= ctx.addedToProgressTotalCount;
                ctx.reportOverallProgress();
            }

            ctx.isLoaded = true;
        });
    }

    public reportOverallProgress() {
        var ctx = this;

        if (!LoaderProgress.reportOverallProgressTimeoutHandle) {
            LoaderProgress.reportOverallProgressTimeoutHandle = window.setTimeout(() => {
                ctx.config.notifyProgress(LoaderProgress.progressDoneCount, LoaderProgress.progressTotalCount);
                LoaderProgress.reportOverallProgressTimeoutHandle = null;
            }, 0);
        }
    }
}